昨天的 Day 12,我們成功讓智慧健康助理加入 BMI 分類功能。
現在系統不只能計算 BMI,還可以根據數值顯示對應的分類。
不過仔細想想,目前的功能都比較偏向「計算」。
如果想讓智慧健康助理真的幫忙管理日常生活,是不是應該開始加入紀錄功能?
所以今天決定挑戰一個新的功能:
每日飲水量紀錄!
這次不只是接收資料後回傳結果,而是要讓 Java 暫時記住使用者輸入的飲水量,並計算累積總量。
今天的目標是:
利用 Spring Boot、POST API 和 Java List,建立簡單的每日飲水量紀錄功能。
一開始規劃智慧健康助理時,就希望它不只是 BMI 計算器,而是可以逐漸加入不同的健康管理功能。
例如:
其中飲水量紀錄相對容易入門。
因為需要處理的資料很簡單:
飲水量(ml)
例如:
早上喝水 300 ml
中午喝水 500 ml
下午喝水 250 ml
系統就可以計算:
300 + 500 + 250 = 1050 ml
最後回傳:
{
"totalWater": 1050
}
這樣就完成一個簡單的紀錄功能!
今天先規劃兩個 API。
| HTTP Method | API | 功能 |
|---|---|---|
| POST | /health/water |
新增飲水紀錄 |
| GET | /health/water |
查詢當日累積飲水量 |
例如使用者傳送:
{
"amount": 300
}
系統就會新增一筆 300 ml 的飲水紀錄。
接著再傳送:
{
"amount": 500
}
系統就會累積成:
800 ml
不過今天還不會使用資料庫。
所以我們先利用 Java 的 List,將資料暫時儲存在記憶體裡。
這代表只要 Spring Boot 重新啟動,紀錄就會消失。
目前先把重點放在理解資料如何新增、儲存和計算。
今天會使用到 Java Collection Framework 裡面的:
List
List 可以用來儲存多筆資料。
例如:
List<Integer> waterRecords = new ArrayList<>();
這代表建立一個可以存放整數的 List。
接著可以加入資料:
waterRecords.add(300);
waterRecords.add(500);
waterRecords.add(250);
現在 List 裡面就有:
[300, 500, 250]
如果想取得所有資料,就可以使用迴圈:
for (int amount : waterRecords) {
System.out.println(amount);
}
輸出:
300
500
250
所以今天我們就可以利用 List 來保存每一次的飲水量。
前幾天已經學會使用 DTO 接收 JSON。
今天也使用相同的方式。
首先建立:
WaterRequest.java
程式如下:
package com.example.healthassistant.dto;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
public class WaterRequest {
@NotNull(message = "Water amount is required.")
@Positive(message = "Water amount must be greater than 0.")
private Integer amount;
public WaterRequest() {
}
public Integer getAmount() {
return amount;
}
public void setAmount(Integer amount) {
this.amount = amount;
}
}
這裡使用了 Day 11 學過的:
@NotNull
以及:
@Positive
避免使用者沒有輸入飲水量,或輸入 0、負數。
例如:
{
"amount": -300
}
就不應該被接受。
另外,這裡使用 Integer 而不是 int,也是為了讓 @NotNull 可以判斷使用者是否漏掉欄位。
接下來是今天最重要的部分。
我們要建立:
WaterService.java
負責管理飲水紀錄。
完整程式如下:
package com.example.healthassistant.service;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@Service
public class WaterService {
private final List<Integer> waterRecords = new ArrayList<>();
private LocalDate recordDate = LocalDate.now();
private void resetIfNewDay() {
LocalDate today = LocalDate.now();
if (!today.equals(recordDate)) {
waterRecords.clear();
recordDate = today;
}
}
public synchronized void addWater(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException(
"Water amount must be greater than 0."
);
}
resetIfNewDay();
waterRecords.add(amount);
}
public synchronized int getTotalWater() {
resetIfNewDay();
int total = 0;
for (int amount : waterRecords) {
total += amount;
}
return total;
}
}
這次的程式比之前稍微長一點,但其實可以拆成幾個部分理解。
先看這段:
private final List<Integer> waterRecords = new ArrayList<>();
它會建立一個 List,用來存放飲水量。
接著:
public synchronized void addWater(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException(
"Water amount must be greater than 0."
);
}
resetIfNewDay();
waterRecords.add(amount);
}
這個 Method 負責新增紀錄。
例如呼叫:
addWater(300);
List 就會變成:
[300]
再呼叫:
addWater(500);
就會變成:
[300, 500]
這樣就可以把多次飲水紀錄保存起來。
這裡還多了一個:
synchronized
因為 Spring 的 Service 通常會被多個 HTTP Request 共用,而 ArrayList 本身不是執行緒安全的。
所以這次先使用 synchronized,避免多個請求同時修改紀錄時產生基本的並行問題。
不過這仍然只是單機、單一使用者的練習版本,還不是正式的多使用者資料儲存設計。
接下來是:
public synchronized int getTotalWater() {
resetIfNewDay();
int total = 0;
for (int amount : waterRecords) {
total += amount;
}
return total;
}
這裡使用 Java 的:
for
迴圈,將 List 裡面的資料加總。
例如:
waterRecords = [300, 500, 250]
計算過程:
total = 0
total = 0 + 300
total = 300 + 500
total = 800 + 250
最後:
total = 1050
因此 Method 就會回傳:
1050
這樣就完成累積飲水量計算!
既然是「每日飲水量」,就不能一直把昨天的飲水量累積到今天。
所以今天還加入一個簡單的日期判斷。
private LocalDate recordDate = LocalDate.now();
LocalDate 是 Java 用來表示日期的類別。
例如:
2026-10-17
接著建立:
private void resetIfNewDay() {
LocalDate today = LocalDate.now();
if (!today.equals(recordDate)) {
waterRecords.clear();
recordDate = today;
}
}
它會檢查今天的日期是否和上一次紀錄的日期相同。
如果日期不同,就執行:
waterRecords.clear();
清空舊的飲水紀錄。
例如:
10/17
飲水總量:1500 ml
到了:
10/18
下一次新增或查詢時,就會先清空前一天的資料。
不過這種做法只保留當日紀錄,無法查詢歷史資料。
而且日期是依照伺服器的時區判斷,未來如果系統有不同地區的使用者,就需要進一步處理時區問題。
Service 完成之後,接下來要建立 API。
建立:
WaterController.java
完整程式如下:
package com.example.healthassistant.controller;
import com.example.healthassistant.dto.WaterRequest;
import com.example.healthassistant.service.WaterService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/health/water")
public class WaterController {
private final WaterService waterService;
public WaterController(WaterService waterService) {
this.waterService = waterService;
}
@PostMapping
public ResponseEntity<Map<String, Object>> addWater(
@Valid @RequestBody WaterRequest request) {
waterService.addWater(request.getAmount());
Map<String, Object> result = new HashMap<>();
result.put("message", "Water record added successfully.");
result.put("amount", request.getAmount());
result.put("totalWater", waterService.getTotalWater());
return ResponseEntity.ok(result);
}
@GetMapping
public ResponseEntity<Map<String, Object>> getWater() {
Map<String, Object> result = new HashMap<>();
result.put("totalWater", waterService.getTotalWater());
result.put("unit", "ml");
return ResponseEntity.ok(result);
}
}
這裡出現一個新的 Annotation:
@RequestMapping("/health/water")
它可以設定整個 Controller 共用的路徑。
所以:
@PostMapping
對應:
POST /health/water
而:
@GetMapping
對應:
GET /health/water
這樣就不用在每個 Method 裡面重複寫完整路徑。
接下來重新啟動 Spring Boot。
先測試新增資料。
HTTP Method:
POST
URL:
http://localhost:8080/health/water
Body:
{
"amount": 300
}
預期回傳:
{
"message": "Water record added successfully.",
"amount": 300,
"totalWater": 300
}
接著再送出第二次:
{
"amount": 500
}
預期回傳:
{
"message": "Water record added successfully.",
"amount": 500,
"totalWater": 800
}
這代表我們的 List 已經成功累積資料。
接著測試第二個 API。
HTTP Method:
GET
URL:
http://localhost:8080/health/water
這次不需要傳送 Body。
如果剛剛新增了:
300 ml
500 ml
預期回傳:
{
"totalWater": 800,
"unit": "ml"
}
這樣就完成了兩個 API:
POST → 新增紀錄
GET → 查詢總量
也終於讓智慧健康助理開始具備基本的資料紀錄功能!
今天新增了三個 Java Class。
目前的專案可以整理成:
health-assistant
│
├── src/main/java/com/example/healthassistant
│ │
│ ├── HealthAssistantApplication.java
│ │
│ ├── controller
│ │ ├── HealthController.java
│ │ └── WaterController.java
│ │
│ ├── service
│ │ ├── BmiService.java
│ │ └── WaterService.java
│ │
│ ├── dto
│ │ ├── HealthRequest.java
│ │ └── WaterRequest.java
│ │
│ └── exception
│ └── GlobalExceptionHandler.java
│
└── pom.xml
現在我們的系統已經有兩種不同的健康功能:
HealthController
↓
BmiService
↓
計算與分類 BMI
WaterController
↓
WaterService
↓
List 儲存飲水量
↓
計算累積總量
這也讓我更能理解之前學習 Controller 與 Service 分層的重要性。
如果所有功能都寫在同一個 Controller 裡面,現在應該已經開始變得很混亂了 XD
雖然今天成功建立飲水量紀錄功能,但目前還有一些限制。
因為 List 是儲存在程式記憶體裡。
只要 Spring Boot 重新啟動,資料就會消失。
目前跨日就會清空資料。
所以無法查詢:
昨天喝了多少水?
或是:
上週平均飲水量是多少?
目前還沒有帳號系統。
因此如果不同使用者呼叫相同 API,飲水紀錄會被放在同一個 List 裡。
這顯然不適合真正的健康管理系統。
目前的設計只適合用來練習。
如果未來希望保存多天紀